﻿using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Collections.Generic;

namespace AdvantIQ.SilverPool
{
    public class Ball
    {
        // Member variables
        public double X;	// Centre of ball
        public double Y;	// Centre of ball
        public double Direction { get; set; }	// Direction (in radians)
        public double Speed { get; set; }	// Speed (in pixels/s)
        public bool IsInHole { get; set; }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        public Ball(double x, double y)
        {
            X = x;
            Y = y;
            IsInHole = false;
            Speed = 0;
            Direction = 0;
        }

        /// <summary>
        /// Give the ball a knock in direction (dx, dy)
        /// </summary>
        /// <param name="dx"></param>
        /// <param name="dy"></param>
        public void Knock(double dx, double dy)
        {
            // Calculate new speed and direction
            Speed = Math.Sqrt(dx * dx + dy * dy);
            if (dy >= 0)
                Direction = Math.Acos(dx / Speed);
            else
                Direction = 2 * Math.PI - Math.Acos(dx / Speed);
        }

        /// <summary>
        /// Check all kinds of collision and update direction if necessary
        /// </summary>
        public void CheckCollisions(IEnumerable<Ball> otherBalls, Rect walls, IEnumerable<Hole> holes)
        {
            _CheckHoleCollision(holes);
            if (!IsInHole)
            {
                _CheckBallCollision(otherBalls);
                _CheckWallCollision(walls);
            }
        }

        /// <summary>
        /// Check collision with other balls
        /// </summary>
        protected void _CheckBallCollision(IEnumerable<Ball> otherBalls)
        {
            foreach (var ball in otherBalls)
            {
                // Skip collision check with self
                if (ball.Equals(this))
                    continue;

                // Calculate distance and check for collision with other ball
                double dx = ball.X - X;
                double dy = ball.Y - Y;
                double d = Math.Sqrt(dx * dx + dy * dy);

                if (d <= Constants.BallDiameter)
                {
                    // Check velocity vector compared to collision direction
                    // Do not knock if ball is moving away from target
                    double tmpDir1, tmpDir2;
                    double reldx = Speed * Math.Cos(Direction) - ball.Speed * Math.Cos(ball.Direction);
                    double reldy = Speed * Math.Sin(Direction) - ball.Speed * Math.Sin(ball.Direction);
                    double reld = Math.Sqrt(reldx * reldx + reldy * reldy);
                    if (reldy >= 0)
                        tmpDir1 = Math.Acos(reldx / reld);
                    else
                        tmpDir1 = 2 * Math.PI - Math.Acos(reldx / reld);
                    if (dy >= 0)
                        tmpDir2 = Math.Acos(dx / d);
                    else
                        tmpDir2 = 2 * Math.PI - Math.Acos(dx / d);
                    if (Math.Cos(tmpDir1 - tmpDir2) < 0)
                        continue;

                    // Unit vector in the collision direction
                    double ax = dx / d;
                    double ay = dy / d;

                    // Calculate velocity vector projection on the new unit vector
                    double va1 = Speed * Math.Cos(Direction) * ax + Speed * Math.Sin(Direction) * ay;
                    double vb1 = -Speed * Math.Cos(Direction) * ay + Speed * Math.Sin(Direction) * ax;
                    double va2 = ball.Speed * Math.Cos(ball.Direction) * ax + ball.Speed * Math.Sin(ball.Direction) * ay;
                    double vb2 = -ball.Speed * Math.Cos(ball.Direction) * ay + ball.Speed * Math.Sin(ball.Direction) * ax;

                    // Calculate new velocities after collision
                    double vaP1 = va1 + (1 + Constants.KnockElasticity) * (va2 - va1) / 2;
                    double vaP2 = va2 + (1 + Constants.KnockElasticity) * (va1 - va2) / 2;

                    // Undo the projections
                    double vx1 = vaP1 * ax - vb1 * ay;
                    double vy1 = vaP1 * ay + vb1 * ax;
                    double vx2 = vaP2 * ax - vb2 * ay;
                    double vy2 = vaP2 * ay + vb1 * ax;

                    // Knock
                    Knock(vx1, vy1);
                    ball.Knock(vx2, vy2);
                }
            }
        }

        /// <summary>
        /// Check collision with walls
        /// </summary>
        protected void _CheckWallCollision(Rect walls)
        {
            // Check left wall
            if (X <= walls.Left + Constants.BallDiameter / 2
                && Speed * Math.Cos(Direction) < 0)
            {
                Direction = _NormalizeDirection(Math.PI - Direction);
            }

            // Check right wall
            if (X >= walls.Right - Constants.BallDiameter / 2
                && Speed * Math.Cos(Direction) > 0)
            {
                Direction = _NormalizeDirection(Math.PI - Direction);
            }

            // Check upper wall
            if (Y <= walls.Top + Constants.BallDiameter / 2
                && Speed * Math.Sin(Direction) < 0)
            {
                Direction = _NormalizeDirection(-Direction);
            }

            // Check lower wall
            if (Y >= walls.Bottom - Constants.BallDiameter / 2
                && Speed * Math.Sin(Direction) > 0)
            {
                Direction = _NormalizeDirection(-Direction);
            }
        }

        /// <summary>
        /// Check if ball has bumped into a hole
        /// </summary>
        protected void _CheckHoleCollision(IEnumerable<Hole> holes)
        {
            foreach (var hole in holes)
            {
                // Calculate distance
                double dx = hole.X - X;
                double dy = hole.Y - Y;
                double d = Math.Sqrt(dx * dx + dy * dy);

                // Check if within hole
                if (d < Constants.HoleDiameter / 2)
                    IsInHole = true;
            }
        }

        /// <summary>
        /// Normalize direction so that it is always between [0, 2*PI]
        /// </summary>
        /// <param name="d"></param>
        /// <returns></returns>
        protected static double _NormalizeDirection(double d)
        {
            if (d < 0)
            {
                int i = (int)(-d / (2 * Math.PI));
                return d + 2 * Math.PI * (i + 1);
            }
            else if (d >= 2 * Math.PI)
            {
                int i = (int)(d / (2 * Math.PI));
                return d - 2 * Math.PI * i;
            }

            return d;
        }

        /// <summary>
        /// Move ball
        /// </summary>
        /// <returns>True if ball was not still</returns>
        public bool Move()
        {
            if (Speed > 0)
            {
                X += Speed * Math.Cos(Direction);
                Y += Speed * Math.Sin(Direction);
                return true;
            }

            return false;
        }

        /// <summary>
        /// Reduce speed by friction
        /// </summary>
        public void ApplyFriction()
        {
            // Reduce speed
            Speed = Speed * (1 - Constants.Friction);

            // Stop ball if speed is low enough
            if (Speed < Constants.MinSpeed && Speed > -Constants.MinSpeed)
                Speed = 0;
        }

        /// <summary>
        /// Check which ball is on a given (x, y) coordinate. Searches ball array on board.
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="board"></param>
        /// <returns></returns>
        public static Ball FindBall(int x, int y, IEnumerable<Ball> balls)
        {
            foreach (var ball in balls)
            {
                double dx = x - ball.X;
                double dy = y - ball.Y;
                double d = Math.Sqrt(dx * dx + dy * dy);

                if (d < Constants.BallDiameter / 2)
                    return ball;
            }

            return null;
        }

        /// <summary>
        /// Check if new ball could be inserted at (x, y) position. Checks against ball array on board.
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="board"></param>
        /// <returns></returns>
        public static bool IsInsertable(double x, double y, IEnumerable<Ball> balls, Rect walls)
        {
            foreach (var ball in balls)
            {
                double dx = x - ball.X;
                double dy = y - ball.Y;
                double d = Math.Sqrt(dx * dx + dy * dy);

                if (d < Constants.BallDiameter)
                    return false;
            }

            // Check left wall
            if (x < walls.Left + Constants.BallDiameter / 2)
                return false;

            // Check right wall
            if (x > walls.Right - Constants.BallDiameter / 2)
                return false;

            // Check upper wall
            if (y < walls.Top + Constants.BallDiameter / 2)
                return false;

            // Check lower wall
            if (y > walls.Bottom - Constants.BallDiameter / 2)
                return false;

            return true;
        }
    }
}
